package appml

import (
	"archive/zip"
	"io"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"gitee.com/kitfast/resml"
)

// 禁止升级：当业务协程调用时，表示该业务运行时不得升级；
// 业务完成后，务必使用 EnableUpgrade() 来表示业务运行完毕，允许升级。
func (m *AppManager) DisableUpgrade() {
	m.upgradable.Add(1) // 注：不能使用 m.mu.Lock() 加锁，否则可能会阻塞业务逻辑
}

// 允许升级：表示业务运行完毕，允许升级。
func (m *AppManager) EnableUpgrade() {
	m.upgradable.Add(-1)
}

// 定时轮询：升级检查
func (m *AppManager) upgradeTick() {
	for range m.upgradeTicker.C {
		m.upgrade()
	}
}

// 升级检查
func (m *AppManager) upgrade() {
	m.mu.Lock()
	defer m.mu.Unlock()
	log.Print("开始检查升级")
	if m.upgradable.Load() > 0 {
		return
	}
	changedResource := m.upgradedResources()
	if len(changedResource) == 0 {
		return
	}
	var upgradeSelf *ConfigApp
	for _, app := range m.config.Apps {
		changed := false
		for _, dep := range app.Deploy {
			if _, changed = changedResource[dep.Resource]; changed {
				break
			}
		}
		if changed && app.Name == m.config.Name {
			upgradeSelf = app // 升级自身
		} else if changed {
			m.upgradeApp(app, changedResource)
		}
	}
	if upgradeSelf != nil {
		m.upgradeSelfPrepare(upgradeSelf, changedResource)
	}
}

// 升级自身的辅助进程的路径
func (m *AppManager) upgradeSelfNewPath(path string) string {
	return filepath.Join(filepath.Dir(path), "new_"+filepath.Base(path))
}

// 升级自身的准备
func (m *AppManager) upgradeSelfPrepare(app *ConfigApp, resources map[string]*ConfigResource) {
	now := time.Now()
	exe, _ := os.Executable()
	other := false
	var self *ConfigDeploy
	for _, dep := range app.Deploy {
		res, changed := resources[dep.Resource]
		if !changed {
			continue
		}
		if app.Process[0] == dep.Deploy || exe == dep.Deploy {
			if res.IsPackage() {
				log.Printf("self resource is a package, can not upgrade self. app: %s resource: %s(%s)", app.Name, res.Name, res.Uri)
			} else {
				self = dep // 应用程序自身，先记下
			}
			continue
		}
		// 除应用程序自身外的其它资源都升级，但只要有失败，则中断升级；已经升级成功的资源暂不理会（不要求版本强一致性）。
		if err := m.upgradeAppDeploy(now, app, dep, res); err != nil {
			return
		}
		other = true
	}
	if self == nil { // 应用程序自身，没有升级，可立即返回
		if other {
			m.procmgr.Restart(app.Name) // 重启
		}
		return
	}
	self.Deploy = m.upgradeSelfNewPath(self.Deploy)
	os.Remove(self.Deploy) // 确保删除之前的文件
	if err := m.upgradeDeployOnly(self, resources[self.Resource]); err != nil {
		log.Printf("[old process] upgrade self failed: %v", err)
		return
	}
	os.Chmod(self.Deploy, 0755) // 赋予可执行权限
	args := make([]string, 0, len(app.Process)+1)
	args = append(args, self.Deploy) // 使用新版本创建升级的辅助进程
	args = append(args, app.Process[1:]...)
	args = append(args, upgrader_argv)
	p, err := NewProcess(app.Name, app.Work, args...)
	if err != nil {
		log.Printf("[old process] upgrade self failed: %v", err)
		return
	}
	if err := p.Start(); err != nil {
		log.Printf("[old process] upgrade self failed: %v", err)
		return
	}
	time.Sleep(3 * time.Second)
	log.Print("[old process] exit for self upgrade.")
	os.Exit(0)
}

// 升级自身，马上执行（由辅助进程执行）
func (m *AppManager) upgradeSelfNow() {
	app := m.config.FindApp(m.config.Name)
	if app == nil {
		log.Printf("[new process] upgrade self failed: can not find app name [%s]", m.config.Name)
		return
	}
	dep := app.FindDeployByPath(app.Process[0])
	if dep == nil {
		log.Printf("[new process] upgrade self failed: can not find deploy path [%s]", app.Process[0])
		return
	}
	res := m.config.FindResource(dep.Resource)
	if res == nil {
		log.Printf("[new process] upgrade self failed: can not find resource name [%s]", dep.Resource)
		return
	}
	if err := m.upgradeAppDeploy(time.Now(), app, dep, res); err != nil { // 部署新版本
		log.Printf("[new process] upgrade self failed: %v", err)
		return
	}
	p, err := NewProcess(app.Name, app.Work, app.Process...)
	if err != nil {
		log.Printf("[new process] upgrade self failed: %v", err)
		return
	}
	if err := p.Start(); err != nil {
		log.Printf("[new process] upgrade self failed: %v", err)
		return
	}
	time.Sleep(3 * time.Second)
	log.Print("[new process] exit for self upgrade.")
	os.Exit(0)
}

// 升级指定应用：停止进程、下载并安装新版本、启动进程
func (m *AppManager) upgradeApp(app *ConfigApp, resources map[string]*ConfigResource) {
	log.Printf("准备升级：%s", app.Name)
	if err := m.procmgr.Kill(app.Name); err != nil { // 停止 App
		log.Printf("kill %s failed: %v", app.Name, NewErr(runtime.Caller(0)).Err(err))
		return
	}
	now := time.Now() // 开始升级的时间，用于备份
	for _, dep := range app.Deploy {
		res, changed := resources[dep.Resource]
		if changed {
			m.upgradeAppDeploy(now, app, dep, res)
		}
	}
	if err := m.procmgr.Start(app.Name); err != nil { // 启动 App
		log.Printf("start %s failed: %v", app.Name, NewErr(runtime.Caller(0)).Err(err))
		return
	}
}

// 升级指定应用的某个部署：备份、安装新版本；失败回滚
func (m *AppManager) upgradeAppDeploy(now time.Time, app *ConfigApp, deploy *ConfigDeploy, res *ConfigResource) error {
	if err := m.upgradeAppDeployBackup(now, app, deploy); err != nil {
		log.Printf("backup %s failed: %v", app.Name, NewErr(runtime.Caller(0)).Err(err))
		return err
	}
	if err := m.upgradeDeployOnly(deploy, res); err != nil {
		log.Printf("upgrade %s failed: %v", app.Name, NewErr(runtime.Caller(0)).Err(err))
		m.upgradeAppDeployRestore(now, app, deploy)
		return err
	}
	return nil
}

// 升级指定应用的某个部署：下载并安装新版本
func (m *AppManager) upgradeDeployOnly(deploy *ConfigDeploy, res *ConfigResource) error {
	u, err := url.Parse(res.Uri)
	if err != nil {
		return NewErr(runtime.Caller(0)).Err(err)
	}
	rr, meta, err := m.upgradeClient.Get(u).Get()
	if err != nil {
		return NewErr(runtime.Caller(0)).Err(err)
	}
	defer rr.Close()
	if !res.IsPackage() {
		w, err := resml.FileCreate(deploy.Deploy, false)
		if err != nil {
			return NewErr(runtime.Caller(0)).Err(err)
		}
		_, err = io.Copy(w, rr)
		w.Close()
		if err != nil {
			os.Remove(deploy.Deploy)
		}
		return NewErr(runtime.Caller(0)).If(err)
	}
	switch res.Type {
	default:
		return NewErr(runtime.Caller(0)).Msg("unsupported type: %s", res.Type)
	case "zip":
		if err = m.upgradeDeployOnlyZip(rr, meta.ContentLength(), deploy); err != nil {
			return NewErr(runtime.Caller(0)).Err(err)
		}
		return nil
	}
}

// 升级指定应用的某个部署：下载并安装新版本的 zip 包
func (m *AppManager) upgradeDeployOnlyZip(rr io.Reader, size int64, deploy *ConfigDeploy) error {
	ra, ok := rr.(io.ReaderAt)
	if !ok {
		return NewErr(runtime.Caller(0)).Msg("not support zip reader, the reader must implement the io.ReaderAt")
	}
	zr, err := zip.NewReader(ra, size)
	if err != nil {
		return NewErr(runtime.Caller(0)).Err(err)
	}
	for _, f := range zr.File {
		if f.FileInfo().IsDir() {
			continue
		}
		name := CleanRelatedPath(f.Name)
		if !IncludePath(name, deploy.Files) {
			continue
		}
		dst := filepath.Join(deploy.Deploy, strings.TrimPrefix(name, deploy.Prefix))
		if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
			os.RemoveAll(deploy.Deploy)
			return NewErr(runtime.Caller(0)).Err(err)
		}
		rc, err := f.Open()
		if err != nil {
			os.RemoveAll(deploy.Deploy)
			return NewErr(runtime.Caller(0)).Err(err)
		}
		fd, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			rc.Close()
			os.RemoveAll(deploy.Deploy)
			return NewErr(runtime.Caller(0)).Err(err)
		}
		_, err = io.Copy(fd, rc)
		fd.Close()
		rc.Close()
		if err != nil {
			os.RemoveAll(deploy.Deploy)
			return NewErr(runtime.Caller(0)).Err(err)
		}
	}
	return nil
}

// 升级指定应用的某个部署：备份
func (m *AppManager) upgradeAppDeployBackup(now time.Time, app *ConfigApp, deploy *ConfigDeploy) error {
	stat, err := os.Stat(deploy.Deploy)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // 未部署过，无需备份
		}
		return NewErr(runtime.Caller(0)).Err(err)
	}
	backupDir := m.config.GetBackupDir(now, app.Name, deploy.Name)
	mkdir := backupDir
	topth := backupDir
	if stat.IsDir() {
		mkdir = filepath.Dir(backupDir)
	} else {
		topth = filepath.Join(backupDir, filepath.Base(deploy.Deploy))
	}
	if err := os.MkdirAll(mkdir, 0755); err != nil {
		return NewErr(runtime.Caller(0)).Err(err)
	}
	if err := os.Rename(deploy.Deploy, topth); err != nil {
		return NewErr(runtime.Caller(0)).Err(err)
	}
	return nil
}

// 升级指定应用的某个部署：回滚
func (m *AppManager) upgradeAppDeployRestore(now time.Time, app *ConfigApp, deploy *ConfigDeploy) {
	backupDir := m.config.GetBackupDir(now, app.Name, deploy.Name)
	if stat, err := os.Stat(backupDir); err != nil {
		if os.IsNotExist(err) {
			return
		}
		log.Printf("[restore] stat backup dir: %v", NewErr(runtime.Caller(0)).Err(err))
		return
	} else if !stat.IsDir() {
		log.Printf("[restore] backup dir %s is not a dir", backupDir)
		return
	}
	file := filepath.Join(backupDir, filepath.Base(deploy.Deploy))
	if stat, err := os.Stat(file); err == nil && !stat.IsDir() {
		if err = os.Remove(deploy.Deploy); err != nil {
			if !os.IsNotExist(err) {
				log.Printf("[restore] remove %s failed: %v", deploy.Deploy, NewErr(runtime.Caller(0)).Err(err))
				return
			}
		}
		if err = os.Rename(file, deploy.Deploy); err != nil {
			log.Printf("[restore] rename %s to %s failed: %v", file, deploy.Deploy, NewErr(runtime.Caller(0)).Err(err))
			return
		}
		if err = DirRemoveEmpties(backupDir, m.config.Backup); err != nil {
			log.Printf("[restore] remove empty dirs %s failed: %v", deploy.Deploy, NewErr(runtime.Caller(0)).Err(err))
		}
		return
	}
	if err := os.RemoveAll(deploy.Deploy); err != nil {
		log.Printf("[restore] remove %s failed: %v", deploy.Deploy, NewErr(runtime.Caller(0)).Err(err))
		return
	}
	if err := os.Rename(backupDir, deploy.Deploy); err != nil {
		log.Printf("[restore] rename %s to %s failed: %v", backupDir, deploy.Deploy, NewErr(runtime.Caller(0)).Err(err))
		return
	}
	if err := DirRemoveEmpties(backupDir, m.config.Backup); err != nil {
		log.Printf("[restore] remove empty dirs %s failed: %v", deploy.Deploy, NewErr(runtime.Caller(0)).Err(err))
	}
}

// 检测是否有升级包
func (m *AppManager) upgradedResources() map[string]*ConfigResource {
	resources := make(map[string]*ConfigResource)
	for _, res := range m.config.Resources {
		u, err := url.Parse(res.Uri)
		if err != nil {
			log.Printf("parse resource uri error: %v", NewErr(runtime.Caller(0)).Err(err))
			continue
		}
		rmeta, err := m.upgradeClient.Get(u).Meta() // 远程资源的meta
		if err != nil {
			log.Printf("get remote resource meta error: %v", NewErr(runtime.Caller(0)).Err(err))
			continue
		}
		// 本地资源的meta与该对象的reader
		// 注：一定要用 Get 来确定文件对象是否存在，因为文件对象可能会被不可控的因素删除掉。
		lreader, lmeta, err := m.upgradeLocal.Get(u.Path)
		if err != nil { // 本地资源不存在(meta或对象任一不存在)，需要更新
			resources[res.Name] = res
			continue
		}
		lreader.Close()
		if !resml.MetaEtagSizeEquals(rmeta, lmeta) { // ETag 或 文件大小 不相等，需要更新
			resources[res.Name] = res
		}
	}
	return resources
}
